package data;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.*;
import org.xml.sax.SAXException;

import data.Data.*;
import exception.TeamException;

/**
 * Represents a team in the game, from a data perspective.
 * 
 * @author Julien
 * 
 */
public class Team implements Serializable {
	private static final long serialVersionUID = -2813822909540357151L;
	private File m_file = null;

	// Team info
	private String m_name = "Unnamed Team";
	private Race m_race = Race.UNKNOWN;

	private int m_numberOfScrimmage = 0;
	private int m_numberOfLeftWideZone = 0;
	private int m_numberOfRightWideZone = 0;

	private int m_score;

	/**
	 * Team id.
	 */
	private transient TeamID id;

	/**
	 * Getter of "id" property.
	 * 
	 * @return the id
	 */
	public TeamID getId() {
		return id;
	}

	/**
	 * Setter of "id" property.
	 * 
	 * @param id
	 *            the id to set
	 */
	public void setId(TeamID id) {
		this.id = id;
	}

	public void setScore(int newScore) {
		m_score = newScore;
	}

	public int getScore() {
		return m_score;
	}

	private boolean m_hasThrown = false;

	public boolean hasThrown() {
		return m_hasThrown;
	}

	public void setHasThrown() {
		m_hasThrown = true;
	}

	private boolean m_hasBlitzed = false;

	public boolean hasBlitzed() {
		return m_hasBlitzed;
	}

	public void setHasBlitzed() {
		m_hasBlitzed = true;
	}

	// static counter, used to assign default team names
	private static int m_teamNumber = 0;

	public void setLeftWideZone(int num) {
		m_numberOfLeftWideZone = num;
	}

	public int getLeftWideZone() {
		return m_numberOfLeftWideZone;
	}

	public void setRightWideZone(int num) {
		m_numberOfRightWideZone = num;
	}

	public int getRightWideZone() {
		return m_numberOfRightWideZone;
	}

	public void setScrimmageNum(int num) {
		m_numberOfScrimmage = num;
	}

	public int getScrimmageNum() {
		return m_numberOfScrimmage;
	}

	public String getName() {
		return m_name;
	}

	public void setName(String name) {
		m_name = name;
	}

	public Race getRace() {
		return m_race;
	}

	public void setRace(Race race) throws TeamException {
		if (m_players.size() > 0)
			throw new TeamException(
					"Not possible to change team race unless the team is empty!");

		m_race = race;
	}

	private List<Player> m_players = new ArrayList<Player>();
	final static private TeamRestrictions m_restrictions = new TeamRestrictions();

	/**
	 * Returns a "read-only" player list, to enable the class to manage the team
	 * limits (i.e. >= 0 and < 16, and other restrictions...
	 */
	public List<Player> players() {
		return Collections.unmodifiableList(m_players);
	}

	/**
	 * Adds the player to the team, validating the team restrictions (number in
	 * team, role limit, race..)
	 * 
	 * @param player
	 *            The player to add to the team.
	 * @throws TeamException
	 */
	public void addPlayer(Player player) throws TeamException {
		if (m_players.size() >= 16)
			throw new TeamException("The team is already full");

		if (getRace() != player.race())
			throw new TeamException(
					"Cannot add a player of another race to this team");

		int currentCount = countPlayerRole(player.role());
		int limit = m_restrictions.limit(player.type());

		if (currentCount >= limit)
			throw new TeamException(
					"Cannot add this player because the limit of that kind is already attained");

		m_players.add(player);
	}

	/**
	 * Removes the player from the team.
	 * 
	 * @param player
	 *            The player to remove.
	 * @throws TeamException
	 */
	public void removePlayer(Player player) throws TeamException {
		if (!m_players.contains(player))
			throw new TeamException("Team does not contain this player");

		m_players.remove(player);
	}

	/**
	 * Removes the dead players from the team roster. To be called at the end of
	 * the game, before saving the teams.
	 */
	public void removeDeadPlayers() {
		ArrayList<Player> players = new ArrayList<Player>(m_players);
		for (Player player : players)
			if (player.state().equals(PlayerState.DEAD))
				m_players.remove(player);
	}

	/**
	 * Check whether current Team has ball or not.
	 * 
	 * @param player
	 *            The player to Check.
	 */
	public boolean hasBall() {
		for (Player player : m_players)
			if (player.hasBall())
				return true;

		return false;
	}

	/**
	 * Gets the player in the team with the given number
	 * 
	 * @param number
	 * @return Player object if there is a player with that number; null if
	 *         there are no players with that number
	 */
	public Player getPlayerByNumber(int number) {
		for (Player player : m_players)
			if (player.number() == number)
				return player;

		return null;
	}

	/**
	 * Returns a "valid" player number that doesn't collide with any player
	 * number currently used in the team.
	 * 
	 * @return A valid player number.
	 */
	public int getUnusedPlayerNumber() {
		int number = 0;
		while (getPlayerByNumber(number) != null)
			number++;

		return number;
	}

	/**
	 * Checks whether the team is ready to play, e.g. that has enough players.
	 * 
	 * @return True if the team has enough players
	 */
	public boolean isTeamReadyToPlay() {
		// We're already guaranteed that there is no more than 16 players.
		return (m_players.size() >= 11);
	}

	/**
	 * Default constructor. Creates a human team with a default unique name.
	 */
	public Team() {
		m_name = "Human Team " + m_teamNumber;
		m_race = Race.HUMAN;
	}

	/**
	 * Creates an empty team of the specified name and race.
	 * 
	 * @param name
	 *            Name of the team to create.
	 * @param race
	 *            Race of the team to create.
	 */
	public Team(String name, Race race) {
		setName(name);
		try {
			setRace(race);
		} catch (TeamException e) {
			// Will not happen.
			e.printStackTrace();
		}
	}

	/**
	 * Creates a team object from a filename
	 * 
	 * @param filename
	 *            The filename representing the file that contains the team XML
	 *            data.
	 * @throws IOException
	 * @throws SAXException
	 */
	public Team(String filename) throws IOException, SAXException {
		this(new File(filename));
	}

	/**
	 * Creates a team object from a File
	 * 
	 * @param xmlFile
	 * @throws IOException
	 * @throws SAXException
	 */
	public Team(File xmlFile) throws IOException, SAXException {
		load(xmlFile);
	}

	/**
	 * Saves the team to the given DOM Document.
	 * 
	 * @param doc
	 *            The document to save the team to.
	 */
	public void save(Document doc) {
		// First, create the "root" which is the team
		Element root = doc.createElement("Team");
		doc.appendChild(root);

		// Then add the team data
		root.setAttribute("Name", getName());
		root.setAttribute("Race", getRace().toString());

		// Then save all players
		for (Player player : players()) {
			player.save(doc, root);
		}
	}

	/**
	 * Saves the team to a file.
	 * 
	 * @param xmlFile
	 */
	public void save(File xmlFile) {
		try {
			// Create factory, builder, document
			DocumentBuilderFactory docFactory = DocumentBuilderFactory
					.newInstance();
			DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
			Document doc = docBuilder.newDocument();

			// Save the team to the document
			save(doc);

			// Save the document to the file using a transformer
			TransformerFactory tFactory = TransformerFactory.newInstance();
			tFactory.setAttribute("indent-number", 2);
			Transformer transformer = tFactory.newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");

			DOMSource source = new DOMSource(doc);
			StreamResult result = new StreamResult(xmlFile);
			transformer.transform(source, result);

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Saves the team using the file it was loaded from.
	 */
	public void save() {
		if (m_file != null)
			save(m_file);
	}

	/**
	 * Loads the team from a DOM Document.
	 * 
	 * @param doc
	 *            The document to load the team from.
	 */
	public void load(Document doc) {
		Element root = doc.getDocumentElement();
		m_name = root.getAttribute("Name");
		m_race = Race.valueOf(root.getAttribute("Race"));

		// Re-initialize the players
		m_players = new ArrayList<Player>();

		NodeList nodeList = root.getElementsByTagName("Player");
		for (int n = 0; n < nodeList.getLength(); n++) {
			Node node = nodeList.item(n);
			if (node.getNodeType() != Node.ELEMENT_NODE)
				continue;

			Element element = (Element) node;
			m_players.add(new Player(element));
		}
	}

	/**
	 * Loads the team from a File, which should contain XML data representing
	 * the team.
	 * 
	 * @param xmlFile
	 *            The file from which we will load the team
	 * @throws IOException
	 * @throws SAXException
	 */
	public void load(File xmlFile) throws IOException, SAXException {
		// Open document from file
		DocumentBuilderFactory docFactory = DocumentBuilderFactory
				.newInstance();

		Document doc = null;
		try {
			DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
			doc = docBuilder.parse(xmlFile);
			doc.getDocumentElement().normalize();

			// Perform the load operation on the loaded document
			load(doc);

			m_file = xmlFile;
		} catch (SAXException e) {
			throw new SAXException(
					"Unable to parse file correctly (malformed XML).", e);
		} catch (IOException e) {
			throw new IOException("Unable to open file.", e.getCause());
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Removes all players and puts back the default team data, according to the
	 * selected race.
	 */
	public void resetToDefault() {
		m_players.clear();
		if (m_race.equals(Race.HUMAN)) {
			// The default human team contains 2 blitzers, 2 catchers, 2
			// throwers and 6 linemen
			m_players.add(new Player("Blitzer-A", Race.HUMAN, Role.BLITZER, 0));
			m_players.add(new Player("Blitzer-B", Race.HUMAN, Role.BLITZER, 1));

			m_players
					.add(new Player("Catcher-A", Race.HUMAN, Role.CATCHER, 10));
			m_players
					.add(new Player("Catcher-B", Race.HUMAN, Role.CATCHER, 11));

			m_players
					.add(new Player("Thrower-A", Race.HUMAN, Role.THROWER, 20));
			m_players
					.add(new Player("Thrower-B", Race.HUMAN, Role.THROWER, 21));

			m_players
					.add(new Player("Lineman-A", Race.HUMAN, Role.LINEMAN, 50));
			m_players
					.add(new Player("Lineman-B", Race.HUMAN, Role.LINEMAN, 51));
			m_players
					.add(new Player("Lineman-C", Race.HUMAN, Role.LINEMAN, 52));
			m_players
					.add(new Player("Lineman-D", Race.HUMAN, Role.LINEMAN, 53));
			m_players
					.add(new Player("Lineman-E", Race.HUMAN, Role.LINEMAN, 54));
			m_players
					.add(new Player("Lineman-F", Race.HUMAN, Role.LINEMAN, 55));
		} else if (m_race.equals(Race.ORC)) {
			// The default orc team contains 2 blitzers, 2 blockers, 2 throwers
			// and 6 linemen
			m_players.add(new Player("Blitzer-A", Race.ORC, Role.BLITZER, 0));
			m_players.add(new Player("Blitzer-B", Race.ORC, Role.BLITZER, 1));

			m_players.add(new Player("Blocker-A", Race.ORC, Role.BLOCKER, 10));
			m_players.add(new Player("Blocker-B", Race.ORC, Role.BLOCKER, 11));

			m_players.add(new Player("Thrower-A", Race.ORC, Role.THROWER, 20));
			m_players.add(new Player("Thrower-B", Race.ORC, Role.THROWER, 21));

			m_players.add(new Player("Lineman-A", Race.ORC, Role.LINEMAN, 50));
			m_players.add(new Player("Lineman-B", Race.ORC, Role.LINEMAN, 51));
			m_players.add(new Player("Lineman-C", Race.ORC, Role.LINEMAN, 52));
			m_players.add(new Player("Lineman-D", Race.ORC, Role.LINEMAN, 53));
			m_players.add(new Player("Lineman-E", Race.ORC, Role.LINEMAN, 54));
			m_players.add(new Player("Lineman-F", Race.ORC, Role.LINEMAN, 55));
		} else if (m_race.equals(Race.ELF)) {
			// The default elf team isn't described in the rulebook, as far as I
			// know.
			// We will assume it is the same as the HUMAN team! I.e. 6 linemen,
			// 2 blitzers, 2 throwers and 2 catchers
			m_players.add(new Player("Blitzer-A", Race.ELF, Role.BLITZER, 0));
			m_players.add(new Player("Blitzer-B", Race.ELF, Role.BLITZER, 1));

			m_players.add(new Player("Catcher-A", Race.ELF, Role.CATCHER, 10));
			m_players.add(new Player("Catcher-B", Race.ELF, Role.CATCHER, 11));

			m_players.add(new Player("Thrower-A", Race.ELF, Role.THROWER, 20));
			m_players.add(new Player("Thrower-B", Race.ELF, Role.THROWER, 21));

			m_players.add(new Player("Lineman-A", Race.ELF, Role.LINEMAN, 50));
			m_players.add(new Player("Lineman-B", Race.ELF, Role.LINEMAN, 51));
			m_players.add(new Player("Lineman-C", Race.ELF, Role.LINEMAN, 52));
			m_players.add(new Player("Lineman-D", Race.ELF, Role.LINEMAN, 53));
			m_players.add(new Player("Lineman-E", Race.ELF, Role.LINEMAN, 54));
			m_players.add(new Player("Lineman-F", Race.ELF, Role.LINEMAN, 55));
		} else {
			// Race not supported yet; no default there
		}
	}

	/**
	 * Counts the number of players in the team with the given role. Used for
	 * team restrictions check.
	 * 
	 * @param role
	 *            The role to check the number of.
	 * @return The number of players having that role
	 */
	private int countPlayerRole(Role role) {
		int nbPlayers = 0;
		for (Player player : m_players) {
			if (player.role().equals(role))
				nbPlayers++;
		}

		return nbPlayers;
	}

	/**
	 * Returns the number of players with a specified state in the team.
	 * 
	 * @param state
	 *            - the state used to filter the players
	 * @return Returns the number of players with a common state.
	 */
	public int countPlayersByState(PlayerState state) {
		int nbPlayers = 0;
		for (Player player : m_players) {
			if (player.state().equals(state))
				nbPlayers++;
		}

		return nbPlayers;
	}

	/**
	 * Counts all the selected players in the team.
	 * 
	 * @return Returns the number of players selected.
	 */
	public int countSelectedPlayers() {
		int nbSelected = 0;
		for (Player player : m_players)
			if (player.isSelected())
				nbSelected++;

		return nbSelected;
	}

	/**
	 * Gets the selected player in the team.
	 * 
	 * @return Returns a reference to the selected player.
	 */
	public Player getSelectedPlayer() {
		for (Player p : m_players) {
			if (p.isSelected())
				return p;
		}
		return null;
	}

	/**
	 * Checks whether two teams are the same, by comparing team name, race and
	 * players.
	 * 
	 * @return True if equal, false otherwise
	 */
	public boolean equals(Object o) {
		if (this == o)
			return true;

		if (!(o instanceof Team))
			return false;

		Team other = (Team) o;

		boolean isEqual = true;

		isEqual &= (m_name.equals(other.m_name));
		isEqual &= (m_race.equals(other.m_race));
		isEqual &= (m_players.size() == other.m_players.size());

		int nbPlayers = m_players.size();
		for (int i = 0; i < nbPlayers && isEqual; i++) {
			isEqual &= m_players.get(i).equals(other.m_players.get(i));
		}

		return isEqual;
	}

	/**
	 * The hashCode must be overriden if the equals method is overriden.
	 * 
	 * @return hashCode
	 */
	public int hashCode() {
		int hash = 17;
		hash = 37 * hash + m_name.hashCode();
		hash = 37 * hash + m_race.hashCode();
		return hash;
	}

	/**
	 * Resets state variables for the team, usually called at end turn.
	 * 
	 * @param turn
	 *            Turn counter, needed to check for STUNNED state
	 */
	public void resetState(int turn) {
		m_hasThrown = false;
		m_hasBlitzed = false;

		// Reset player states
		for (Player p : m_players) {
			p.setHasActed(false);
			p.setHasBlitzed(false);
			p.resetMA();
			p.select(false);
			p.hasUsedDodge(false);

			if (p.state().equals(PlayerState.STUNNED)
					&& turn > p.wasStunnedOnTurn()) {
				p.setState(PlayerState.PRONE);
			}
		}
	}
}

/**
 * Class that holds the team membership restrictions. Is used when adding new
 * players in a team.
 * 
 * @author Julien
 * 
 */
class TeamRestrictions {
	Map<PlayerType, Integer> m_restrictions = new HashMap<PlayerType, Integer>();

	TeamRestrictions() {
		// Information taken from the team data in the rulebook.
		m_restrictions.put(new PlayerType(Race.HUMAN, Role.LINEMAN), Integer
				.valueOf(16));
		m_restrictions.put(new PlayerType(Race.HUMAN, Role.CATCHER), Integer
				.valueOf(4));
		m_restrictions.put(new PlayerType(Race.HUMAN, Role.THROWER), Integer
				.valueOf(2));
		m_restrictions.put(new PlayerType(Race.HUMAN, Role.BLITZER), Integer
				.valueOf(4));

		m_restrictions.put(new PlayerType(Race.ORC, Role.LINEMAN), Integer
				.valueOf(16));
		m_restrictions.put(new PlayerType(Race.ORC, Role.THROWER), Integer
				.valueOf(2));
		m_restrictions.put(new PlayerType(Race.ORC, Role.BLOCKER), Integer
				.valueOf(4));
		m_restrictions.put(new PlayerType(Race.ORC, Role.BLITZER), Integer
				.valueOf(4));

		m_restrictions.put(new PlayerType(Race.ELF, Role.LINEMAN), Integer
				.valueOf(16));
		m_restrictions.put(new PlayerType(Race.ELF, Role.CATCHER), Integer
				.valueOf(4));
		m_restrictions.put(new PlayerType(Race.ELF, Role.THROWER), Integer
				.valueOf(2));
		m_restrictions.put(new PlayerType(Race.ELF, Role.BLITZER), Integer
				.valueOf(2));
	}

	/**
	 * Returns limit of players of that type in a team
	 * 
	 * @param playerType
	 *            the player type wanted
	 * @return the limit
	 */
	int limit(PlayerType playerType) {
		if (m_restrictions.containsKey(playerType))
			return m_restrictions.get(playerType).intValue();
		else
			return 0;
	}
}